/*
 * Copyright (C) 2014 Miquel Sas
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package traderlib.core.data.db;

import java.util.ArrayList;
import traderlib.core.data.types.KeyPointer;
import java.util.HashMap;
import java.util.List;
import traderlib.core.data.entity.Field;
import traderlib.core.data.entity.FieldList;
import traderlib.core.data.entity.Order;
import traderlib.core.data.types.OrderKey;
import traderlib.core.data.types.Value;

/**
 * A database table definition.
 *
 * @author Miquel Sas
 */
public class Table implements Comparable<Object> {

	/**
	 * The name of the view.
	 */
	private String name = null;
	/**
	 * An optional description.
	 */
	private String description = null;
	/**
	 * The alias.
	 */
	private String alias = null;
	/**
	 * The database schema.
	 */
	private String schema = null;
	/**
	 * The list of columns.
	 */
	private final FieldList columns = new FieldList();
	/**
	 * A flag that indicates if this table is persistent.
	 */
	private boolean persistent = true;
	/**
	 * A flag that indicates if this table has persistent constraints.
	 */
	private boolean persistentConstraints = true;
	/**
	 * The primary key.
	 */
	private Index primaryKey = null;
	/**
	 * The list of secondary indexes.
	 */
	private final List<Index> indexes = new ArrayList<>();
	/**
	 * The list offoreign keys.
	 */
	private final List<ForeignKey> foreignKeys = new ArrayList<>();
	/**
	 * A cache of the lis of primary key columns
	 */
	private List<Column> primaryKeyColumns = null;

	/**
	 * Default constructor.
	 */
	public Table() {
		super();
	}

	/**
	 * Get the name.
	 *
	 * @return The table name
	 */
	public String getName() {
		return name;
	}

	/**
	 * Gets the name to use in <code>FROM</code> clause.
	 *
	 * @return The appropriate name for a <code>FROM</code> clause.
	 */
	public String getNameFrom() {
		if (alias != null && name != null) {
			return getNameSchema() + " " + alias;
		}
		return getNameSchema();
	}

	/**
	 * Gets the name qualified with the schema.
	 *
	 * @return The name qualified with the schema.
	 */
	public String getNameSchema() {
		if (schema != null && name != null) {
			return schema + "." + name;
		}
		return name;
	}

	/**
	 * Set the name.
	 *
	 * @param name The table name
	 */
	public void setName(String name) {
		this.name = null;
		if (name != null && name.trim().length() > 0) {
			this.name = name.trim();
		}
	}

	/**
	 * Returns the optional description.
	 *
	 * @return The description.
	 */
	public String getDescription() {
		return description;
	}

	/**
	 * Set the description.
	 *
	 * @param description The optional description.
	 */
	public void setDescription(String description) {
		this.description = description;
	}

	/**
	 * Gets the alias.
	 *
	 * @return The alias.
	 */
	public String getAlias() {
		if (alias == null) {
			return name;
		}
		return alias;
	}

	/**
	 * Sets the alias.
	 *
	 * @param alias The table alias.
	 */
	public void setAlias(String alias) {
		this.alias = null;
		if (alias != null && alias.trim().length() > 0) {
			this.alias = alias;
		}
	}

	/**
	 * Get the schema.
	 *
	 * @return The table schema
	 */
	public String getSchema() {
		return schema;
	}

	/**
	 * Set the schema.
	 *
	 * @param schema
	 */
	public void setSchema(String schema) {
		this.schema = schema;
	}

	/**
	 * Clear the column map, necessary when changing columns.
	 */
	public void clearColumnMap() {
		columns.clearFieldMap();
		if (primaryKey != null) {
			primaryKey.clear();
		}
		primaryKey = null;
		if (primaryKeyColumns != null) {
			primaryKeyColumns.clear();
		}
		primaryKeyColumns = null;
		indexes.clear();
		foreignKeys.clear();
	}

	/**
	 * Compares this table with the argument table. Returns 0 if they are equal, -1 if this value is less than the
	 * argument, and 1 if it is greater. A table is considered to be equal to another table in the context of a
	 * <code>SELECT</code> statement.
	 *
	 * @return An int
	 * @param o The object to compare with.
	 */
	@Override
	public int compareTo(Object o) {
		Table table = null;
		try {
			table = (Table) o;
		} catch (ClassCastException exc) {
			throw new UnsupportedOperationException("Not comparable type: " + o.getClass().getName());
		}
		return getNameFrom().compareTo(table.getNameFrom());
	}

	/**
	 * Indicates whether an object is "equal to" this table. A table is considered to be equal to another table in the
	 * context of a SELECT statement, that is, with name from.
	 *
	 * @return A boolean
	 * @param o The object to compare with.
	 */
	@Override
	public boolean equals(Object o) {
		if (o == null) {
			return false;
		}
		if (getClass() != o.getClass()) {
			return false;
		}
		final Table table = (Table) o;
		return getNameFrom().equals(table.getNameFrom());
	}

	/**
	 * Returns the hash code for this value. A table is considered to have the same hash code than another table in the
	 * context of a SELECT statement.
	 *
	 * @return The hash code
	 */
	@Override
	public int hashCode() {
		return getNameFrom().hashCode();
	}

	/**
	 * Add a column to the column list.
	 *
	 * @param column The column to add.
	 */
	public void addColumn(Column column) {
		column.setParentTable(this);
		columns.addField(column);
		clearColumnMap();
	}

	/**
	 * Returns the number of columns in this table.
	 *
	 * @return The number of columns.
	 */
	public int getColumnCount() {
		return columns.size();
	}

	/**
	 * Returns the column in the given index.
	 *
	 * @param index The index of the column.
	 * @return The column
	 */
	public Column getColumn(int index) {
		return (Column) columns.getField(index);
	}

	/**
	 * Get a column by alias.
	 *
	 * @return The column or null if not found.
	 * @param alias The column alias.
	 */
	public Column getColumn(String alias) {
		return (Column) columns.getField(alias);
	}

	/**
	 * Get a column index by alias.
	 *
	 * @return The column index or -1 if not found.
	 * @param alias The column alias.
	 */
	public int getColumnIndex(String alias) {
		return columns.getFieldIndex(alias);
	}

	/**
	 * Get a column index.
	 *
	 * @return The column index or -1 if not found.
	 * @param column The column.
	 */
	public int getColumnIndex(Column column) {
		return getColumnIndex(column.getAlias());
	}

	/**
	 * Returns the columns, for use in the friend class Cursor.
	 *
	 * @return The column list.
	 */
	public FieldList getColumnList() {
		return columns;
	}

	/**
	 * Removes all the columns in this table.
	 *
	 * @return The removed columns
	 */
	public List<Column> removeAllColumns() {
		clearColumnMap();
		List<Column> removedColumns = new ArrayList<>();
		for (int i = 0; i < getColumnCount(); i++) {
			removedColumns.add(getColumn(i));
		}
		columns.clear();
		return removedColumns;
	}

	/**
	 * Remove the column at the given index.
	 *
	 * @param index The index of the column
	 * @return The removed column or null.
	 */
	public Column removeColumn(int index) {
		clearColumnMap();
		return (Column) columns.removeField(index);
	}

	/**
	 * Remove the with the given alias.
	 *
	 * @param alias The alias of the column
	 * @return The removed column or null.
	 */
	public Column removeColumn(String alias) {
		int index = getColumnIndex(alias);
		if (index < 0) {
			return null;
		}
		return removeColumn(index);
	}

	/**
	 * Returns the list of primary key columns.
	 *
	 * @return The list of primary key columns.
	 */
	public List<Column> getPrimaryKeyColumns() {
		if (primaryKeyColumns == null) {
			primaryKeyColumns = new ArrayList<>();
			List<Field> primaryKeyFields = columns.getPrimaryKeyFields();
			for (Field field : primaryKeyFields) {
				primaryKeyColumns.add((Column) field);
			}
		}
		return primaryKeyColumns;
	}

	/**
	 * Check if this table is persistend.
	 *
	 * @return A boolean
	 */
	public boolean isPersistent() {
		return persistent;
	}

	/**
	 * Set if this table is persistent.
	 *
	 * @param persistent A boolean
	 */
	public void setPersistent(boolean persistent) {
		this.persistent = persistent;
	}

	/**
	 * Check if this tablehas persistent constraints.
	 *
	 * @return A bboolean
	 */
	public boolean isPersistentConstraints() {
		if (!isPersistent()) {
			return false;
		}
		return persistentConstraints;
	}

	/**
	 * Set if this table has persistent constraints.
	 *
	 * @param persistentConstraints A boolean
	 */
	public void setPersistentConstraints(boolean persistentConstraints) {
		this.persistentConstraints = persistentConstraints;
	}

	/**
	 * Returns the primary key index.
	 *
	 * @return The primary key index
	 */
	public Index getPrimaryKey() {
		if (primaryKey == null) {
			primaryKey = new Index();
			primaryKey.setTable(this);
			primaryKey.setUnique(true);
			// Default name
			primaryKey.setName(getName() + "_PK");
			primaryKey.setSchema(getSchema());
			primaryKey.setDescription(getName() + " primary key");
			List<KeyPointer> primaryKeyPointers = columns.getPrimaryKeyPointers();
			for (KeyPointer pointer : primaryKeyPointers) {
				primaryKey.add(getColumn(pointer.getIndex()));
			}
		}
		return primaryKey;
	}

	/**
	 * Removes the primary key index.
	 */
	public void removePrimaryKey() {
		primaryKey = null;
	}

	/**
	 * Get the list of primary key pointers.
	 *
	 * @return The list of primary key pointers.
	 */
	public List<KeyPointer> getPrimaryKeyPointers() {
		return columns.getPrimaryKeyPointers();
	}

	/**
	 * Add an index to the list ofsecondary indexes.
	 *
	 * @param index The index to add
	 */
	public void addIndex(Index index) {
		index.setTable(this);
		if (index.getName() == null) {
			index.setName(getName() + "_SK" + indexes.size());
		}
		if (index.getSchema() == null) {
			index.setSchema(getSchema());
		}
		if (index.getDescription() == null) {
			index.setDescription(getName() + " secondary index " + indexes.size());
		}
		indexes.add(index);
	}

	/**
	 * Remove all secondary indexes.
	 *
	 * @return The list with the removed indexes.
	 */
	public List<Index> removeAllIndexes() {
		List<Index> removedIndexes = new ArrayList<>();
		removedIndexes.addAll(indexes);
		indexes.clear();
		return removedIndexes;
	}

	/**
	 * Remove an index at a given position.
	 *
	 * @param index The index position
	 * @return The removed index.
	 */
	public Index removeIndex(int index) {
		return indexes.remove(index);
	}

	/**
	 * Returns the number of secondaryindexes.
	 *
	 * @return The number of secondary indexes.
	 */
	public int getIndexCount() {
		return indexes.size();
	}

	/**
	 * Returns the index at the given position.
	 *
	 * @param index The index position.
	 * @return
	 */
	public Index getIndex(int index) {
		return indexes.get(index);
	}

	/**
	 * Add a foreign key to the lsttof foreign keys.
	 *
	 * @param foreignKey The foreign key to add.
	 */
	public void addForeignKey(ForeignKey foreignKey) {
		foreignKeys.add(foreignKey);
	}

	/**
	 * Remove all foreign keys.
	 *
	 * @return The list with the removed foreign keys.
	 */
	public List<ForeignKey> removeAllForeignKeys() {
		List<ForeignKey> removedForeignKeys = new ArrayList<>();
		removedForeignKeys.addAll(foreignKeys);
		foreignKeys.clear();
		return removedForeignKeys;
	}

	/**
	 * Remove and return the foreiggn key at the given position.
	 *
	 * @param index The foreign key position
	 * @return The foreign key
	 */
	public ForeignKey removeForeignKey(int index) {
		return foreignKeys.remove(index);
	}

	/**
	 * Get the foreignkey at the given position.
	 *
	 * @param index The foreign key position in the list
	 * @return The foreign key
	 */
	public ForeignKey getForeignKey(int index) {
		return foreignKeys.get(index);
	}

	/**
	 * Returns the number of foreign keys.
	 *
	 * @return The number of foreign keys.
	 */
	public int getForeignKeyCount() {
		return foreignKeys.size();
	}

	/**
	 * Returns a simple view of this table, using the argument index as the order by index. No relations are build based
	 * on foreign keys.
	 *
	 * @return The most simple view.
	 * @param orderBy The order by index.
	 */
	public View getSimpleView(Order orderBy) {
		View view = new View();
		view.setMasterTable(this);
		for (int i = 0; i < getColumnCount(); i++) {
			Column column = new Column(getColumn(i));
			column.setParentView(view);
			view.addColumn(column);
		}
		view.setOrderBy(orderBy);
		view.setName(getName());
		view.setAlias(getAlias());
		return view;
	}

	/**
	 * Returns a complex view of this table, using the argument order as the order by clause. Relations are build based
	 * on chained foreign keys including all the columns of the related tables, in the same order as returned by
	 * <code>getAllColumns</code>.
	 *
	 * @return The complex view.
	 * @param orderBy The order by index.
	 */
	public View getComplexView(Order orderBy) {
		View view = getSimpleView(orderBy);
		if (getForeignKeyCount() > 0) {
			if (getDescription() != null) {
				view.setDescription(getDescription());
			}
		}
		for (int i = 0; i < getForeignKeyCount(); i++) {
			ForeignKey foreignKey = getForeignKey(i);
			Table foreignTable = foreignKey.getForeignTable();
			Relation relation = new Relation();
			relation.setLocalTable(this);
			relation.setForeignTable(foreignTable);
			relation.setOuter(true);
			HashMap<Column, Column> map = new HashMap<>();
			for (ForeignKey.Segment foreignKeySegment : foreignKey) {
				Relation.Segment relationSegment = new Relation.Segment();
				relationSegment.setLocalColumn(foreignKeySegment.getLocalColumn());
				relationSegment.setForeignColumn(foreignKeySegment.getForeignColumn());
				map.put(relationSegment.getForeignColumn(), relationSegment.getForeignColumn());
				relation.add(relationSegment);
			}
			for (int j = 0; j < foreignTable.getColumnCount(); j++) {
				if (!map.containsKey(foreignTable.getColumn(j))) {
					Column column = new Column(foreignTable.getColumn(j));
					column.setParentView(view);
					view.addColumn(column);
				}
			}
			view.addRelation(relation);
		}
		return view;
	}

	/**
	 * Returns the appropriate filter for this table primery key and its argument record, that corresponds to a record
	 * of the table.
	 *
	 * @param record The record with values from this table.
	 * @return A filter that applies to the primary key.
	 */
	public Filter getPrimaryKeyFilter(Record record) {
		return getPrimaryKeyFilter(record.getPrimaryKey());
	}

	/**
	 * Returns the appropriate filter for this table primery key and its argument key, that corresponds to a record
	 * primary key.
	 *
	 * @param primaryKey
	 * @return The filter.
	 */
	public Filter getPrimaryKeyFilter(OrderKey primaryKey) {
		Filter filter = new Filter();
		List<KeyPointer> primaryKeyPointers = getPrimaryKeyPointers();
		for (int i = 0; i < primaryKeyPointers.size(); i++) {
			KeyPointer pointer = primaryKeyPointers.get(i);
			int index = pointer.getIndex();
			Column column = getColumn(index);
			Value value = primaryKey.get(i).getValue();
			if (i > 0) {
				filter.and();
			}
			filter.condSimple(column, "EQ", value);
		}
		return filter;
	}
}
